﻿using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using JetBrains.Annotations;
using Lint.Exceptions;
using Lint.Extensions;
using Lint.Native.OS;
using Lint.ObjectTranslation;

namespace Lint.Native
{
    /// <summary>
    ///     Represents the Lua library. This class holds delegate instances of all known Lua function signatures, i.e it is in
    ///     charge of communicating with lua53.dll.
    /// </summary>
    internal static class LuaLibrary
    {
        /// <summary>
        ///     Gets the minimum required number of free stack slots. This value is used with lua_checkstack.
        /// </summary>
        public const int LUA_MINSTACK = 20;

        /// <summary>
        ///     Gets the MULTRET value. MULTRET values are used with lua_call and lua_pcall to indicate that all
        ///     function results should be pushed to the stack.
        /// </summary>
        public const int LUA_MULTRET = -1;

        /// <summary>
        ///     Gets the lua_checkstack delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaCheckStack LuaCheckStack;

        /// <summary>
        ///     Gets the lua_close delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaClose LuaClose;

        /// <summary>
        ///     Gets the lua_createtable delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaCreateTable LuaCreateTable;

        /// <summary>
        ///     Gets the lua_getfield delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaGetField LuaGetField;

        /// <summary>
        ///     Gets the lua_getglobal delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaGetGlobal LuaGetGlobal;

        /// <summary>
        ///     Gets the lua_getmetatable delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaGetMetatable LuaGetMetatable;

        /// <summary>
        ///     Gets the lua_getstack delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaGetStack LuaGetStack;

        /// <summary>
        ///     Gets the lua_gettable delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaGetTable LuaGetTable;

        /// <summary>
        ///     Gets the lua_gettop delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaGetTop LuaGetTop;

        /// <summary>
        ///     Gets the lua_isinteger delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaIsInteger LuaIsInteger;

        /// <summary>
        ///     Gets the luaL_loadstring delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaLLoadString LuaLLoadString;

        /// <summary>
        ///     Gets the luaL_newmetatable delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaLNewMetatable LuaLNewMetatable;

        /// <summary>
        ///     Gets the luaL_newstate delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaLNewState LuaLNewState;

        /// <summary>
        ///     Gets the luaL_openlibs delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaLOpenLibs LuaLOpenLibs;

        /// <summary>
        ///     Gets the luaL_ref delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaLRef LuaLRef;

        /// <summary>
        ///     Gets the lua_newthread delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaNewThread LuaNewThread;

        /// <summary>
        ///     Gets the lua_newuserdata delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaNewUserdata LuaNewUserdata;

        /// <summary>
        ///     Gets the lua_next delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaNext LuaNext;

        /// <summary>
        ///     Gets the lua_pcall delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPCallK LuaPCallK;

        /// <summary>
        ///     Gets the lua_pushboolean delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPushBoolean LuaPushBoolean;

        /// <summary>
        ///     Gets the lua_pushcclosure delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPushCClosure LuaPushCClosure;

        /// <summary>
        ///     Gets the lua_pushinteger delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPushInteger LuaPushInteger;

        /// <summary>
        ///     Gets the lua_pushlstring delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPushLString LuaPushLString;

        /// <summary>
        ///     Gets the lua_pushnil delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPushNil LuaPushNil;

        /// <summary>
        ///     Gets the lua_pushnumber delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPushNumber LuaPushNumber;

        /// <summary>
        ///     Gets the lua_pushvalue delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaPushValue LuaPushValue;

        /// <summary>
        ///     Gets the lua_rawgeti delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaRawGetI LuaRawGetI;

        /// <summary>
        ///     Gets the lua_resume delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaResume LuaResume;

        /// <summary>
        ///     Gets the lua_setglobal delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaSetGlobal LuaSetGlobal;

        /// <summary>
        ///     Gets the lua_sethook delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaSetHook LuaSetHook;

        /// <summary>
        ///     Gets the lua_setmetatable delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaSetMetatable LuaSetMetatable;

        /// <summary>
        ///     Gets the lua_settable delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaSetTable LuaSetTable;

        /// <summary>
        ///     Gets the lua_settop delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaSetTop LuaSetTop;

        /// <summary>
        ///     Gets the lua_status delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaStatus LuaStatus;

        /// <summary>
        ///     Gets the lua_toboolean delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaToBoolean LuaToBoolean;

        /// <summary>
        ///     Gets the lua_tointegerx delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaToIntegerX LuaToIntegerX;

        /// <summary>
        ///     Gets the lua_tolstring delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaToLString LuaToLString;

        /// <summary>
        ///     Gets the lua_tonumberx delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaToNumberX LuaToNumberX;

        /// <summary>
        ///     Gets the lua_topointer delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaToPointer LuaToPointer;

        /// <summary>
        ///     Gets the lua_tothread delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaToThread LuaToThread;

        /// <summary>
        ///     Gets the lua_touserdata delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaToUserdata LuaToUserdata;

        /// <summary>
        ///     Gets the lua_type delegate.
        /// </summary>
        private static readonly LuaFunctionDelegates.LuaGetType LuaTypeDelegate;

        /// <summary>
        ///     Gets the lua_xmove delegate.
        /// </summary>
        public static readonly LuaFunctionDelegates.LuaXMove LuaXMove;

        /// <summary>
        ///     Gets the static constructor. This constructor is used to assign static Lua function delegates.
        /// </summary>
        /// <remarks>
        ///     Static constructors are only invoked once per class, which is right before the first instance is created or
        ///     before accessing static fields.
        /// </remarks>
        static LuaLibrary()
        {
            // The former does not account for 32-bit apps and thus attempts to load a 64 bit library
            var architecture = IntPtr.Size == 8 ? "64bit" : "32bit";
            var fileName = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "lua53.so" : "lua53.dll";
            var functionLoader = new FunctionLoader(Path.Combine(
                Path.GetDirectoryName(new Uri(Assembly.GetAssembly(typeof(Engine)).CodeBase).LocalPath) ??
                throw new InvalidOperationException(), "libs", architecture, fileName));

            // TODO: One of these functions results in a nullptr and keeps throwing exceptions [X]
            LuaCheckStack = functionLoader.LoadFunction<LuaFunctionDelegates.LuaCheckStack>("lua_checkstack");
            LuaClose = functionLoader.LoadFunction<LuaFunctionDelegates.LuaClose>("lua_close");
            LuaCreateTable = functionLoader.LoadFunction<LuaFunctionDelegates.LuaCreateTable>("lua_createtable");
            LuaGetField = functionLoader.LoadFunction<LuaFunctionDelegates.LuaGetField>("lua_getfield");
            LuaGetGlobal = functionLoader.LoadFunction<LuaFunctionDelegates.LuaGetGlobal>("lua_getglobal");
            LuaGetMetatable = functionLoader.LoadFunction<LuaFunctionDelegates.LuaGetMetatable>("lua_getmetatable");
            LuaGetStack = functionLoader.LoadFunction<LuaFunctionDelegates.LuaGetStack>("lua_getstack");
            LuaGetTable = functionLoader.LoadFunction<LuaFunctionDelegates.LuaGetTable>("lua_gettable");
            LuaGetTop = functionLoader.LoadFunction<LuaFunctionDelegates.LuaGetTop>("lua_gettop");
            LuaIsInteger = functionLoader.LoadFunction<LuaFunctionDelegates.LuaIsInteger>("lua_isinteger");
            LuaLLoadString = functionLoader.LoadFunction<LuaFunctionDelegates.LuaLLoadString>("luaL_loadstring");
            LuaLNewMetatable = functionLoader.LoadFunction<LuaFunctionDelegates.LuaLNewMetatable>("luaL_newmetatable");
            LuaLNewState = functionLoader.LoadFunction<LuaFunctionDelegates.LuaLNewState>("luaL_newstate");
            LuaLOpenLibs = functionLoader.LoadFunction<LuaFunctionDelegates.LuaLOpenLibs>("luaL_openlibs");
            LuaLRef = functionLoader.LoadFunction<LuaFunctionDelegates.LuaLRef>("luaL_ref");
            LuaNewThread = functionLoader.LoadFunction<LuaFunctionDelegates.LuaNewThread>("lua_newthread");
            LuaNewUserdata = functionLoader.LoadFunction<LuaFunctionDelegates.LuaNewUserdata>("lua_newuserdata");
            LuaNext = functionLoader.LoadFunction<LuaFunctionDelegates.LuaNext>("lua_next");
            LuaPCallK = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPCallK>("lua_pcallk");
            //LuaPCall = functionLoader.LoadFunction<LuaPCall>("lua_pcall"); TODO: Replace with lua_pcallk which is also safer [X]
            //LuaPop = functionLoader.LoadFunction<LuaPop>("lua_pop"); 
            /*
             * For some reason the native file handler cannot access the pop function (10.2.2019)
             *
             * Elaboration: lua_pcall and lua_pop are not actual C functions but macros.
             * lua_pcall invokes the lua_pcallk function with default parameters while
             * lua_pop invokes the lua_settop function and executes specific logic. Since both of these Lua functions are C macros
             * they cannot be marshaled as pointers to the interpreter, which results in an exception being raised when attempting
             * to marshal them. (11.2.2019)
             */
            LuaPushBoolean = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPushBoolean>("lua_pushboolean");
            LuaPushCClosure = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPushCClosure>("lua_pushcclosure");
            LuaPushInteger = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPushInteger>("lua_pushinteger");
            LuaPushLString = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPushLString>("lua_pushlstring");
            LuaPushNil = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPushNil>("lua_pushnil");
            LuaPushNumber = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPushNumber>("lua_pushnumber");
            LuaPushValue = functionLoader.LoadFunction<LuaFunctionDelegates.LuaPushValue>("lua_pushvalue");
            LuaRawGetI = functionLoader.LoadFunction<LuaFunctionDelegates.LuaRawGetI>("lua_rawgeti");
            //LuaReplace = functionLoader.LoadFunction<LuaFunctionDelegates.LuaReplace>("lua_replace");
            LuaResume = functionLoader.LoadFunction<LuaFunctionDelegates.LuaResume>("lua_resume");
            LuaSetGlobal = functionLoader.LoadFunction<LuaFunctionDelegates.LuaSetGlobal>("lua_setglobal");
            LuaSetHook = functionLoader.LoadFunction<LuaFunctionDelegates.LuaSetHook>("lua_sethook");
            LuaSetMetatable = functionLoader.LoadFunction<LuaFunctionDelegates.LuaSetMetatable>("lua_setmetatable");
            LuaSetTable = functionLoader.LoadFunction<LuaFunctionDelegates.LuaSetTable>("lua_settable");
            LuaSetTop = functionLoader.LoadFunction<LuaFunctionDelegates.LuaSetTop>("lua_settop");
            LuaStatus = functionLoader.LoadFunction<LuaFunctionDelegates.LuaStatus>("lua_status");
            LuaToBoolean = functionLoader.LoadFunction<LuaFunctionDelegates.LuaToBoolean>("lua_toboolean");
            LuaToIntegerX = functionLoader.LoadFunction<LuaFunctionDelegates.LuaToIntegerX>("lua_tointegerx");
            LuaToLString = functionLoader.LoadFunction<LuaFunctionDelegates.LuaToLString>("lua_tolstring");
            LuaToNumberX = functionLoader.LoadFunction<LuaFunctionDelegates.LuaToNumberX>("lua_tonumberx");
            LuaToPointer = functionLoader.LoadFunction<LuaFunctionDelegates.LuaToPointer>("lua_topointer");
            LuaToThread = functionLoader.LoadFunction<LuaFunctionDelegates.LuaToThread>("lua_tothread");
            LuaToUserdata = functionLoader.LoadFunction<LuaFunctionDelegates.LuaToUserdata>("lua_touserdata");
            LuaTypeDelegate = functionLoader.LoadFunction<LuaFunctionDelegates.LuaGetType>("lua_type");
            LuaXMove = functionLoader.LoadFunction<LuaFunctionDelegates.LuaXMove>("lua_xmove");
        }

        /// <summary>
        ///     Executes the specified Lua file and returns the results.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="filePath">The file path, which must not be <c>null</c>.</param>
        /// <param name="numberOfResults">The number of results to return.</param>
        /// <returns>The results.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="filePath" /> is <c>null</c>.</exception>
        /// <exception cref="FileNotFoundException"><paramref name="filePath" /> is invalid.</exception>
        /// <exception cref="LuaException">The specified file is not a .lua file.</exception>
        /// <exception cref="LuaScriptException">Something went wrong while executing the file.</exception>
        public static object?[] DoFile(IntPtr luaState, string filePath, int numberOfResults = LUA_MULTRET)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException();
            }

            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            if (Path.GetExtension(fileStream.Name) != ".lua")
            {
                throw new LuaException("Cannot execute a non Lua file.");
            }

            using var binaryReader = new BinaryReader(fileStream);
            var buffer = binaryReader.ReadBytes((int) fileStream.Length);
            LuaThreadStatus errorCode;
            if ((errorCode = (LuaThreadStatus) LuaLLoadString(luaState, buffer)) != LuaThreadStatus.LUA_OK)
            {
                // Lua pushes an error message in case of exceptions
                var errorMessage = (string) ObjectTranslator.GetObject(luaState, -1)!;
                LuaPop(luaState, 1); // Pop the error message and throw an exception
                throw new LuaScriptException($"[{errorCode}]: {errorMessage}");
            }

            return CallWithArguments(luaState, numberOfResults: numberOfResults);
        }

        /// <summary>
        ///     Executes the specified Lua chunk and returns the results.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="luaChunk">The Lua chunk to execute, which must not be <c>null</c>.</param>
        /// <param name="numberOfResults">The number of results to return.</param>
        /// <returns>The chunk's results.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="luaChunk" /> is <c>null</c>.</exception>
        /// <exception cref="LuaScriptException"><paramref name="luaChunk" /> is invalid.</exception>
        public static object?[] DoString(IntPtr luaState, string luaChunk, int numberOfResults = LUA_MULTRET)
        {
            LuaThreadStatus errorCode;
            if ((errorCode = (LuaThreadStatus) LuaLLoadString(luaState, luaChunk.GetEncodedBytes())) !=
                LuaThreadStatus.LUA_OK)
            {
                // Lua pushes an error message in case of errors
                var errorMessage = (string) ObjectTranslator.GetObject(luaState, -1)!;
                LuaPop(luaState, 1); // Pop the error message and throw an exception
                throw new LuaScriptException($"[{errorCode}]: {errorMessage}");
            }

            return CallWithArguments(luaState, numberOfResults: numberOfResults);
        }

        /// <summary>
        ///     Gets the Lua type of the element at the specified index in the stack of the specified Lua state.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="stackIndex">The stack index of the element.</param>
        /// <returns>The corresponding <see cref="LuaType" />.</returns>
        public static LuaType GetLuaType(IntPtr luaState, int stackIndex) =>
            (LuaType) LuaTypeDelegate(luaState, stackIndex);

        /// <summary>
        ///     Pops the specified number of elements from the stack of the specified Lua state.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="numberOfElementsToPop">The number of elements to pop.</param>
        public static void LuaPop(IntPtr luaState, int numberOfElementsToPop)
        {
            LuaSetTop(luaState, -numberOfElementsToPop - 1);
        }

        /// <summary>
        ///     Pushes a C function to the top of the stack of the specified Lua state.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="luaCFunction">The C function.</param>
        /// <remarks>
        ///     All C functions must follow the required syntax; the function takes a Lua state as the only parameter and it
        ///     must return the number of results pushed to the stack.
        /// </remarks>
        public static void LuaPushCFunction(IntPtr luaState, LuaFunctionDelegates.LuaCFunction luaCFunction)
        {
            LuaPushCClosure(luaState, luaCFunction, 0);
        }

        /// <summary>
        ///     Pushes the specified string to the top of the stack of the specified Lua state.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="str">The string to push.</param>
        public static void LuaPushString(IntPtr luaState, string str)
        {
            var bytes = str.GetEncodedBytes(); // Lua uses UTF-8 encoding for strings
            LuaPushLString(luaState, bytes, new UIntPtr((uint) bytes.Length));
        }

        /// <summary>
        ///     Gets the string representation of the element at the specified index in the stack of the specified Lua state.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="index">The stack index of the element.</param>
        /// <returns>The string representation of the element at the specified index.</returns>
        public static string LuaToString(IntPtr luaState, int index)
        {
            var stringPointer = LuaToLString(luaState, index, out var length);
            var buffer = new byte[(byte) length];
            Marshal.Copy(stringPointer, buffer, 0, buffer.Length);
            return Encoding.UTF8.GetString(buffer);
        }

        /// <summary>
        ///     Returns the pseudo-index that represents the i-th upvalue of the function that is currently executing.
        /// </summary>
        /// <param name="upvalue">The upvalue.</param>
        /// <returns>The pseudo-index representing the i-th upvalue.</returns>
        public static int LuaUpvalueIndex(int upvalue) => (int) LuaRegistry.RegistryIndex - upvalue;

        /// <summary>
        ///     Calls the function at the top of the stack with the specified arguments.
        /// </summary>
        /// <param name="luaState">The Lua state pointer.</param>
        /// <param name="arguments">The function's arguments.</param>
        /// <param name="numberOfResults">The number of results to be pushed to the stack. Defaults to all results.</param>
        /// <returns>The function's results.</returns>
        /// <exception cref="LuaException">Something went wrong while executing the function.</exception>
        public static object?[] CallWithArguments(IntPtr luaState, object[]? arguments = null,
            int numberOfResults = LUA_MULTRET)
        {
            // The function (which is currently at the top of the stack) gets popped along with the arguments when it's called
            var stackTop = LuaGetTop(luaState) - 1;

            // The function is already on the stack so the only thing left to do is push the arguments in direct order
            if (arguments != null)
            {
                if (!LuaCheckStack(luaState, arguments.Length))
                {
                    throw new LuaException("The stack does not have enough space to allocate that many arguments.");
                }

                foreach (var argument in arguments)
                {
                    ObjectTranslator.PushToStack(luaState, argument);
                }
            }

            // Adjust the number of results to avoid errors
            numberOfResults = numberOfResults < -1 ? -1 : numberOfResults;
            LuaThreadStatus errorCode;
            if ((errorCode =
                    (LuaThreadStatus) LuaPCallK(luaState, arguments?.Length ?? 0, numberOfResults)) !=
                LuaThreadStatus.LUA_OK)
            {
                // Lua pushes an error message in case of errors
                var errorMessage = (string) ObjectTranslator.GetObject(luaState, -1)!;
                LuaPop(luaState, 1); // Pop the error message and throw an exception
                throw new LuaException(
                    $"An exception has occured while calling a function: [{errorCode}]: {errorMessage}");
            }

            var newStackTop = LuaGetTop(luaState);
            var results = new object?[newStackTop - stackTop];
            for (var i = newStackTop; i > stackTop; --i) // Results are also pushed in direct order
            {
                results[i - stackTop - 1] = ObjectTranslator.GetObject(luaState, i);
            }

            // As per Lua's reference manual, resetting the stack to it's original form is considered good programming practice
            LuaSetTop(luaState, stackTop);
            return results;
        }
    }
}